 /*******************************************************************************
  * Copyright (c) 2004, 2005 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.properties;

 import java.io.*;
 import java.util.*;
 import org.eclipse.core.internal.localstore.Bucket;
 import org.eclipse.core.internal.resources.ResourceException;
 import org.eclipse.core.internal.utils.Messages;
 import org.eclipse.core.resources.IResourceStatus;
 import org.eclipse.core.runtime.*;
 import org.eclipse.osgi.util.NLS;

 public class PropertyBucket extends Bucket {
     public static class PropertyEntry extends Entry {

         private final static Comparator COMPARATOR = new Comparator() {
             public int compare(Object o1, Object o2) {
                 int qualifierComparison = ((String []) o1)[0].compareTo(((String []) o2)[0]);
                 return qualifierComparison != 0 ? qualifierComparison : ((String []) o1)[1].compareTo(((String []) o2)[1]);
             }
         };
         private static final String [][] EMPTY_DATA = new String [0][];
         /**
          * value is a String[][] of {{propertyKey.qualifier, propertyKey.localName, propertyValue}}
          */
         private String [][] value;

         /**
          * Deletes the property with the given name, and returns the result array. Returns the original
          * array if the property to be deleted could not be found. Returns <code>null</code> if the property was found
          * and the original array had size 1 (instead of a zero-length array).
          */
         static String [][] delete(String [][] existing, QualifiedName propertyName) {
             // a size-1 array is a special case
 if (existing.length == 1)
                 return (existing[0][0].equals(propertyName.getQualifier()) && existing[0][1].equals(propertyName.getLocalName())) ? null : existing;
             // find the guy to delete
 int deletePosition = search(existing, propertyName);
             if (deletePosition < 0)
                 // not found, nothing to delete
 return existing;
             String [][] newValue = new String [existing.length - 1][];
             if (deletePosition > 0)
                 // copy elements preceding the one to be removed
 System.arraycopy(existing, 0, newValue, 0, deletePosition);
             if (deletePosition < existing.length - 1)
                 // copy elements succeeding the one to be removed
 System.arraycopy(existing, deletePosition + 1, newValue, deletePosition, newValue.length - deletePosition);
             return newValue;
         }

         static String [][] insert(String [][] existing, QualifiedName propertyName, String propertyValue) {
             // look for the right spot where to insert the new guy
 int index = search(existing, propertyName);
             if (index >= 0) {
                 // found existing occurrence - just replace the value
 existing[index][2] = propertyValue;
                 return existing;
             }
             // not found - insert
 int insertPosition = -index - 1;
             String [][] newValue = new String [existing.length + 1][];
             if (insertPosition > 0)
                 System.arraycopy(existing, 0, newValue, 0, insertPosition);
             newValue[insertPosition] = new String [] {propertyName.getQualifier(), propertyName.getLocalName(), propertyValue};
             if (insertPosition < existing.length)
                 System.arraycopy(existing, insertPosition, newValue, insertPosition + 1, existing.length - insertPosition);
             return newValue;
         }

         /**
          * Merges two entries (are always sorted). Duplicated additions replace existing ones.
          */
         static Object merge(String [][] base, String [][] additions) {
             int additionPointer = 0;
             int basePointer = 0;
             int added = 0;
             String [][] result = new String [base.length + additions.length][];
             while (basePointer < base.length && additionPointer < additions.length) {
                 int comparison = COMPARATOR.compare(base[basePointer], additions[additionPointer]);
                 if (comparison == 0) {
                     result[added++] = additions[additionPointer++];
                     // duplicate, override
 basePointer++;
                 } else if (comparison < 0)
                     result[added++] = base[basePointer++];
                 else
                     result[added++] = additions[additionPointer++];
             }
             // copy the remaining states from either additions or base arrays
 String [][] remaining = basePointer == base.length ? additions : base;
             int remainingPointer = basePointer == base.length ? additionPointer : basePointer;
             int remainingCount = remaining.length - remainingPointer;
             System.arraycopy(remaining, remainingPointer, result, added, remainingCount);
             added += remainingCount;
             if (added == base.length + additions.length)
                 // no collisions
 return result;
             // there were collisions, need to compact
 String [][] finalResult = new String [added][];
             System.arraycopy(result, 0, finalResult, 0, finalResult.length);
             return finalResult;
         }

         private static int search(String [][] existing, QualifiedName propertyName) {
             return Arrays.binarySearch(existing, new String [] {propertyName.getQualifier(), propertyName.getLocalName(), null}, COMPARATOR);
         }

         public PropertyEntry(IPath path, PropertyEntry base) {
             super(path);
             //copy 2-dimensional array [x][y]
 int xLen = base.value.length;
             this.value = new String [xLen][];
             for (int i = 0; i < xLen; i++) {
                 int yLen = base.value[i].length;
                 this.value[i] = new String [yLen];
                 System.arraycopy(base.value[i], 0, value[i], 0, yLen);
             }
         }

         /**
          * @param path
          * @param value is a String[][] {{propertyKey, propertyValue}}
          */
         protected PropertyEntry(IPath path, String [][] value) {
             super(path);
             this.value = value;
         }

         /**
          * Compacts the data array removing any null slots. If non-null slots
          * are found, the entry is marked for removal.
          */
         private void compact() {
             if (!isDirty())
                 return;
             int occurrences = 0;
             for (int i = 0; i < value.length; i++)
                 if (value[i] != null)
                     value[occurrences++] = value[i];
             if (occurrences == value.length)
                 // no states deleted
 return;
             if (occurrences == 0) {
                 // no states remaining
 value = EMPTY_DATA;
                 delete();
                 return;
             }
             String [][] result = new String [occurrences][];
             System.arraycopy(value, 0, result, 0, occurrences);
             value = result;
         }

         public int getOccurrences() {
             return value == null ? 0 : value.length;
         }

         public String getProperty(QualifiedName name) {
             int index = search(value, name);
             return index < 0 ? null : value[index][2];
         }

         public Object getPropertyName(int i) {
             return new QualifiedName(this.value[i][0], this.value[i][1]);
         }

         public Object getPropertyValue(int i) {
             return this.value[i][2];
         }

         public Object getValue() {
             return value;
         }

         public void visited() {
             compact();
         }
     }

     public static final byte INDEX = 1;

     public static final byte QNAME = 2;

     /** Version number for the current implementation file's format.
      * <p>
      * Version 1:
      * <pre>
      * FILE ::= VERSION_ID ENTRY+
      * ENTRY ::= PATH PROPERTY_COUNT PROPERTY+
      * PATH ::= string (does not contain project name)
      * PROPERTY_COUNT ::= int
      * PROPERTY ::= QUALIFIER LOCAL_NAME VALUE
      * QUALIFIER ::= INDEX | QNAME
      * INDEX -> byte int
      * QNAME -> byte string
      * UUID ::= byte[16]
      * LAST_MODIFIED ::= byte[8]
      * </pre>
      * </p>
      */
     private static final byte VERSION = 1;

     private final List qualifierIndex = new ArrayList();

     public PropertyBucket() {
         super();
     }

     protected Entry createEntry(IPath path, Object value) {
         return new PropertyEntry(path, (String [][]) value);
     }

     private PropertyEntry getEntry(IPath path) {
         String pathAsString = path.toString();
         String [][] existing = (String [][]) getEntryValue(pathAsString);
         if (existing == null)
             return null;
         return new PropertyEntry(path, existing);
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.internal.localstore.Bucket#getIndexFileName()
      */
     protected String getIndexFileName() {
         return "properties.index"; //$NON-NLS-1$
 }

     public String getProperty(IPath path, QualifiedName name) {
         PropertyEntry entry = getEntry(path);
         if (entry == null)
             return null;
         return entry.getProperty(name);
     }

     protected byte getVersion() {
         return VERSION;
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.internal.localstore.Bucket#getVersionFileName()
      */
     protected String getVersionFileName() {
         return "properties.version"; //$NON-NLS-1$
 }

     public void load(String newProjectName, File baseLocation, boolean force) throws CoreException {
         qualifierIndex.clear();
         super.load(newProjectName, baseLocation, force);
     }

     protected Object readEntryValue(DataInputStream source) throws IOException, CoreException {
         int length = source.readUnsignedShort();
         String [][] properties = new String [length][3];
         for (int j = 0; j < properties.length; j++) {
             // qualifier
 byte constant = source.readByte();
             switch (constant) {
                 case QNAME :
                     properties[j][0] = source.readUTF();
                     qualifierIndex.add(properties[j][0]);
                     break;
                 case INDEX :
                     properties[j][0] = (String ) qualifierIndex.get(source.readInt());
                     break;
                 default :
                     //if we get here the properties file is corrupt
 IPath resourcePath = projectName == null ? Path.ROOT : Path.ROOT.append(projectName);
                     String msg = NLS.bind(Messages.properties_readProperties, resourcePath.toString());
                     throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, null, msg, null);
             }
             // localName
 properties[j][1] = source.readUTF();
             // propertyValue
 properties[j][2] = source.readUTF();
         }
         return properties;
     }

     public void save() throws CoreException {
         qualifierIndex.clear();
         super.save();
     }

     public void setProperties(PropertyEntry entry) {
         IPath path = entry.getPath();
         String [][] additions = (String [][]) entry.getValue();
         String pathAsString = path.toString();
         String [][] existing = (String [][]) getEntryValue(pathAsString);
         if (existing == null) {
             setEntryValue(pathAsString, additions);
             return;
         }
         setEntryValue(pathAsString, PropertyEntry.merge(existing, additions));
     }

     public void setProperty(IPath path, QualifiedName name, String value) {
         String pathAsString = path.toString();
         String [][] existing = (String [][]) getEntryValue(pathAsString);
         if (existing == null) {
             if (value != null)
                 setEntryValue(pathAsString, new String [][] {{name.getQualifier(), name.getLocalName(), value}});
             return;
         }
         String [][] newValue;
         if (value != null)
             newValue = PropertyEntry.insert(existing, name, value);
         else
             newValue = PropertyEntry.delete(existing, name);
         // even if newValue == existing we should mark as dirty (insert may just change the existing array)
 setEntryValue(pathAsString, newValue);
     }

     protected void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException {
         String [][] properties = (String [][]) entryValue;
         destination.writeShort(properties.length);
         for (int j = 0; j < properties.length; j++) {
             // writes the property key qualifier
 int index = qualifierIndex.indexOf(properties[j][0]);
             if (index == -1) {
                 destination.writeByte(QNAME);
                 destination.writeUTF(properties[j][0]);
                 qualifierIndex.add(properties[j][0]);
             } else {
                 destination.writeByte(INDEX);
                 destination.writeInt(index);
             }
             // then the local name
 destination.writeUTF(properties[j][1]);
             // then the property value
 destination.writeUTF(properties[j][2]);
         }
     }
 }

